/*
 * MainMenu.java
 *
 * Created on November 4, 2006, 5:13 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package edu.umn.cs5115.scheduler.framework;

import edu.umn.cs5115.scheduler.framework.KeyValueObserver.SetMutationType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Set;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;

/**
 * This creates the application's main menu bar.
 * @author grant
 */
public class MainMenu
{
    /** Backspace's key code. */
    private final static int BACKSPACE = 8;
    
    /** Creates a new instance of MainMenu */
    public MainMenu()
    {
    }
    
    /**
     * Create the application's main menu bar.  This sets up some default 
     * behavior and adds it as the specified window's menu bar.
     * @param window The window to create the menu bar for.
     * @param document The currently active document when this window is in the
     * foreground.  It may be null if this is not a document window.
     */
    public void createMenuBar(final Shell window, final Document document)
    {
        final DocumentController sharedController = DocumentController.getSharedDocumentController();
        Menu menuBar = new Menu(window, SWT.BAR);
        window.setMenuBar(menuBar);
        // Create the File Menu
        MenuItem fileMenuItem = new MenuItem(menuBar, SWT.CASCADE);
        fileMenuItem.setText("File");
        Menu fileMenu = new Menu(fileMenuItem);
        fileMenuItem.setMenu(fileMenu);
        
        // fill the file menu.
            // Create the menu item "New"
            MenuItem newMenuItem = new MenuItem(fileMenu, SWT.CASCADE);
            newMenuItem.setAccelerator(SWT.MOD1 | 'N');
            newMenuItem.setText("New");
            newMenuItem.addSelectionListener(new SelectionAdapter()
            {
                public void widgetSelected(SelectionEvent e)
                {
                    sharedController.newDocument();
                }
            });
            
            // Create the menu item "Open"
            MenuItem openMenuItem = new MenuItem(fileMenu, SWT.CASCADE);
            openMenuItem.setAccelerator(SWT.MOD1 | 'O');
            openMenuItem.setText("Open\u2026");
            openMenuItem.addSelectionListener(new SelectionAdapter()
            {
                public void widgetSelected(SelectionEvent e)
                {
                    sharedController.open();
                }
            });
            // Create the menu item "Open Recent"
            MenuItem openRecentMenuItem = new MenuItem(fileMenu, SWT.CASCADE);
            openRecentMenuItem.setText("Open Recent");
            final Menu openRecentMenu = new Menu(openRecentMenuItem);
            openRecentMenuItem.setMenu(openRecentMenu);
                // populate the "Open Recent" menu
                // (we should do the useful part of that here)
                // add the separator, and the clear menu item.
                MenuItem openRecentSeparator = new MenuItem(openRecentMenu, SWT.SEPARATOR);
                
                // Create the "Clear recent items" menu item.
                MenuItem clearRecentItems = new MenuItem(openRecentMenu, SWT.CASCADE);
                clearRecentItems.setText("Clear Menu");
                
            // create the separator before operations to this file
            MenuItem fileSeparatorOne = new MenuItem(fileMenu, SWT.SEPARATOR);
            
            // create "Close" menu item
            MenuItem close = new MenuItem(fileMenu, SWT.CASCADE);
            close.setAccelerator(SWT.MOD1 | 'W');
            close.setText("Close");
            if (document != null)
            {
                close.addSelectionListener(new SelectionAdapter()
                {
                    public void widgetSelected(SelectionEvent e)
                    {
                        window.close();
                    }
                });
            } else {
                close.setEnabled(false);
            }
            
            // create "Save" menu item
            final MenuItem saveMenuItem = new MenuItem(fileMenu, SWT.CASCADE);
            saveMenuItem.setAccelerator(SWT.MOD1 | 'S');
            saveMenuItem.setText("Save");
            if (document != null)
            {
                saveMenuItem.addSelectionListener(new SelectionAdapter()
                {
                    public void widgetSelected(SelectionEvent e)
                    {
                        document.save();
                    }
                });
                final KeyValueObserver unsavedChangesListener = new KeyValueAdapter()
                {
                    public void valueChanged(KeyValueCoding object, String key)
                    {
                        saveMenuItem.setEnabled(document.isSavable());
                    }
                };
                document.addListener(unsavedChangesListener, Document.SAVABLE);
                saveMenuItem.addDisposeListener(new DisposeListener()
                {
                    public void widgetDisposed(DisposeEvent e)
                    {
                        document.removeListener(unsavedChangesListener, Document.SAVABLE);
                    }
                });
                saveMenuItem.setEnabled(document.isSavable());
            } else {
                saveMenuItem.setEnabled(false);
            }
            
            // create "Save As" menu item
            MenuItem saveAsMenuItem = new MenuItem(fileMenu, SWT.CASCADE);
            saveAsMenuItem.setAccelerator(SWT.MOD1 | SWT.SHIFT | 'S');
            saveAsMenuItem.setText("Save As\u2026");
            if (document != null)
            {
                saveAsMenuItem.addSelectionListener(new SelectionAdapter()
                {
                    public void widgetSelected(SelectionEvent e)
                    {
                        document.saveAs();
                    }
                });
            } else {
                saveAsMenuItem.setEnabled(false);
            }
            
            // create "Revert to Saved" menu item
            final MenuItem revertMenuItem = new MenuItem(fileMenu, SWT.CASCADE);
            revertMenuItem.setText("Revert to Saved");
            if (document != null)
            {
                revertMenuItem.addSelectionListener(new SelectionAdapter()
                {
                    public void widgetSelected(SelectionEvent e)
                    {
                        document.revert();
                    }
                });
                final KeyValueObserver revertableListener = new KeyValueAdapter()
                {
                    public void valueChanged(KeyValueCoding object, String key)
                    {
                        revertMenuItem.setEnabled(document.isRevertable());
                    }
                };
                document.addListener(revertableListener, Document.REVERTABLE);
                revertMenuItem.addDisposeListener(new DisposeListener()
                {
                    public void widgetDisposed(DisposeEvent e)
                    {
                        document.removeListener(revertableListener);
                        revertMenuItem.removeDisposeListener(this);
                    }
                });
                revertMenuItem.setEnabled(document.isRevertable());
                
            } else {
                revertMenuItem.setEnabled(false);
            }
            
            // create a divider and a menu item to quit on Linux/Windows
            Application.OS os = Application.getOS();
            if (os != Application.OS.MAC_OS_X)
            {
                // create the separator before operations to this file
                MenuItem fileSeparatorTwo = new MenuItem(fileMenu, SWT.SEPARATOR);

                // create "Quit" menu item
                MenuItem quit = new MenuItem(fileMenu, SWT.CASCADE);
                switch(os)
                {
                    case WINDOWS:
                        quit.setAccelerator(SWT.MOD1 | 'X');
                        quit.setText("Exit");
                        break;
                    default:
                        quit.setAccelerator(SWT.MOD1 | 'Q');
                        quit.setText("Quit");
                }
                quit.addSelectionListener(new SelectionAdapter()
                {
                    public void widgetSelected(SelectionEvent e)
                    {
                        DocumentController.getSharedDocumentController().quit();
                    }
                });
            }
            
        // Create the "Edit" menu
        MenuItem editMenuItem = new MenuItem(menuBar, SWT.CASCADE);
        editMenuItem.setText("Edit");
        Menu editMenu = new Menu(editMenuItem);
        editMenuItem.setMenu(editMenu);
        
        editMenuItem.addSelectionListener(new SelectionListener()
        {
            public void widgetDefaultSelected(SelectionEvent e)
            {
                System.out.println("widgetDefaultSelected");
            }
            public void widgetSelected(SelectionEvent e)
            {
                System.out.println("widgetSelected");
            }
        });
        
        // Fill the "Edit" menu
            // Create the menu item "Undo"
            final MenuItem undoMenuItem = new MenuItem(editMenu, SWT.CASCADE);
            undoMenuItem.setAccelerator(SWT.MOD1 | 'Z');
            undoMenuItem.setText("Undo");
            if (document == null)
                undoMenuItem.setEnabled(false);
            else {
                undoMenuItem.addSelectionListener(new SelectionAdapter()
                {
                    public void widgetSelected(SelectionEvent e)
                    {
                        document.getUndoManager().undo();
                    }
                });
                final KeyValueObserver undoListener = new KeyValueAdapter()
                {
                    public void valueChanged(KeyValueCoding object, String key)
                    {
                        UndoManager undoManager = (UndoManager)object;
                        undoMenuItem.setEnabled(undoManager.canUndo());
                        undoMenuItem.setText("Undo " + undoManager.getUndoActionName());
                    }
                };
                document.getUndoManager().addListener(undoListener, UndoManager.UNDO_KEY);
                undoMenuItem.addDisposeListener(new DisposeListener()
                {
                    public void widgetDisposed(DisposeEvent e)
                    {
                        document.getUndoManager().removeListener(undoListener, UndoManager.UNDO_KEY);
                    }
                });
                undoMenuItem.setEnabled(document.getUndoManager().canUndo());
            }
            // Create the menu item "Redo"
            final MenuItem redoMenuItem = new MenuItem(editMenu, SWT.CASCADE);
            redoMenuItem.setAccelerator(SWT.MOD1 | SWT.SHIFT | 'Z');
            redoMenuItem.setText("Redo");
            if (document == null)
                redoMenuItem.setEnabled(false);
            else {
                redoMenuItem.addSelectionListener(new SelectionAdapter()
                {
                    public void widgetSelected(SelectionEvent e)
                    {
                        document.getUndoManager().redo();
                    }
                });
                final KeyValueObserver redoListener = new KeyValueAdapter()
                {
                    public void valueChanged(KeyValueCoding object, String key)
                    {
                        UndoManager undoManager = (UndoManager)object;
                        redoMenuItem.setEnabled(undoManager.canRedo());
                        redoMenuItem.setText("Redo " + undoManager.getRedoActionName());
                    }
                };
                document.getUndoManager().addListener(redoListener, UndoManager.REDO_KEY);
                undoMenuItem.addDisposeListener(new DisposeListener()
                {
                    public void widgetDisposed(DisposeEvent e)
                    {
                        document.getUndoManager().removeListener(redoListener, UndoManager.REDO_KEY);
                    }
                });
                
                redoMenuItem.setEnabled(document.getUndoManager().canRedo());
            }
            
            // create the separator before normal (non-undo) editing options
            MenuItem editSeparatorOne = new MenuItem(editMenu, SWT.SEPARATOR);
            
            // create "Delete" menu item
            final MenuItem deleteCourse = new MenuItem(editMenu, SWT.CASCADE);
            deleteCourse.setAccelerator(BACKSPACE);
            deleteCourse.setText(getActionName("delete"));
            attachAction(deleteCourse, "delete");
    }
    
    /**
     * Tests whether the action is currently available on the given control.  
     * @param control The control to test.
     * @return true if the control's "actionAvailable" method returns false,
     * false if it returns true or doesn't exist.
     */
    protected boolean actionDisabled(Object control, String actionName)
    {
        try
        {
            Method method = control.getClass().getMethod("actionAvailable", String.class);
            Class returnType = method.getReturnType();
            if (returnType == Boolean.class || returnType == boolean.class)
            {
                if (Modifier.isPublic(method.getModifiers()))
                    method.setAccessible(true);
                try
                {
                    return !((Boolean)method.invoke(control, actionName)).booleanValue();
                } catch (IllegalAccessException ex) {
                    // method is inaccessible - treat it as though it wasn't there.
                } catch (InvocationTargetException ex){
                    System.out.println("Exception ("+ex.getCause().getLocalizedMessage()+") thrown during execution of actionAvailable on object of type " + control.getClass().getName() + ".  Ignored.");
                }
            }
        } catch (NoSuchMethodException ex) {
        }
        
        // if not specifically prohibitted, it's not disabled.
        return false;
    }
    
    /**
     * Looks for methods named "addAvailableListener" and 
     * "removeAvailableListener" on the given object.  If found, it packages 
     * them into a listener set, and returns them.  Otherwise it returns null.
     * @param object The object to check for the methods on.
     * @return A ListenerSet containing invocations for both methods, if they 
     * are found, and null otherwise.
     */
    protected ListenerSet getListenerMethods(Object object)
    {
        try
        {
            ListenerSet set = new ListenerSet();
            set.adder = new Invocation(object, object.getClass().getMethod("addAvailableListener", String.class, MenuAvailabilityListener.class));
            set.remover = new Invocation(object, object.getClass().getMethod("removeAvailableListener", String.class, MenuAvailabilityListener.class));
            if (Modifier.isPublic(set.adder.method.getModifiers()))
                set.adder.method.setAccessible(true);
            if (Modifier.isPublic(set.remover.method.getModifiers()))
                set.remover.method.setAccessible(true);
            return set;
        } catch (NoSuchMethodException ex) {
            // The method doesn't exist, so it's not prohibited.
            return null;
        }
    }
    
    /**
     * Checks for the existence of a method on the given control.
     * @param control The control to examine.
     * @param actionName The name of the action to test.
     * @return The method, if it is found, and null otherwise.
     */
    protected Method findActionMethod(Object control, String actionName)
    {
        try
        {
            return control.getClass().getMethod(actionName);
        } catch (NoSuchMethodException ex) {
            // we can't call the action if it doesn't exist
            return null;
        }
    }
    
    /**
     * Get a customized action name for the given action
     * @param actionName The method name for the action.
     * @return A customized, human-readable string for the action name, or null
     * if no customized action name is provided.
     */
    protected String getActionName(String actionName)
    {
        // Could maybe request this from the currently focused control, if it 
        // provides it.
        if (actionName.equals("delete"))
            return "Delete";
        return null;
    }
    
    /**
     * Automatically attaches an action to a menu item. It will be 
     * automatically updated as the focus changes, and removed when the menu
     * item is disposed.  The action name is sent to the focused control when
     * the menu item is selected.
     * @param menuItem The menu item to attach to.
     * @param actionName The name of the action for this menu.
     */
    protected void attachAction(final MenuItem menuItem, final String actionName)
    {
        final SelectionListener selectionListener = new SelectionAdapter()
        {
            public void widgetSelected(SelectionEvent e)
            {
                Object focused = DocumentController.getSharedDocumentController().getFocusControl();
                Method theMethod = findActionMethod(focused, actionName);
                if (theMethod != null)
                {
                    try
                    {
                        // allow ourself to access public members of private classes.
                        if (Modifier.isPublic(theMethod.getModifiers()))
                            theMethod.setAccessible(true);
                        theMethod.invoke(focused);
                    } catch (IllegalAccessException ex) {
                        ex.printStackTrace();
                    } catch (InvocationTargetException ex) {
                        ex.printStackTrace();
                    }
                } else {
                    menuItem.setEnabled(false);
                    e.doit = false;
                }
            }
        };
        
        menuItem.addSelectionListener(selectionListener);
        
        final FocusListener focusListener = new FocusListener(actionName, menuItem);
        DocumentController.getSharedDocumentController().addListener(focusListener, DocumentController.FOCUS_CONTROL_KEY);
        
        menuItem.addDisposeListener(new DisposeListener()
        {
            public void widgetDisposed(DisposeEvent e)
            {
                focusListener.stopListening();
                menuItem.removeSelectionListener(selectionListener);
                DocumentController.getSharedDocumentController().removeListener(focusListener, DocumentController.FOCUS_CONTROL_KEY);
            }
        });
    }
    
    /** 
     * Presently a simple class to hold a Method and the object on which it
     * should be called.  If needed elsewhere, flesh it out and move it ouotside
     * MainMenu.
     */
    protected static class Invocation
    {
        public Method method;
        public Object target;
        
        /**
         * Just initializes the public fields.
         */
        public Invocation(Object target, Method method)
        {
            this.target = target;
            this.method = method;
        }
        
        /**
         * Calls method.invoke with the given list of parameters.
         */
        public void invoke(Object... parameters) throws IllegalAccessException, InvocationTargetException
        {
            method.invoke(target, parameters);
        }
    }
    
    /**
     * Just a simple class to package two things together.
     */
    protected static class ListenerSet
    {
        public Invocation adder;
        public Invocation remover;
    }

    private class FocusListener extends KeyValueAdapter
    {
        private String actionName;
        private MenuItem menuItem;

        public FocusListener(String actionName, MenuItem menuItem)
        {
            super();
            this.actionName = actionName;
            this.menuItem = menuItem;
        }

        private Object lastFocus;
        private boolean listening;
        private ListenerSet listenerMethods;

        private MenuAvailabilityListener availabilityListener = new MenuAvailabilityListener()
        {

            public void availabilityChanged()
            {
                updateAvailable();
            }
        };

        public void valueChanged(KeyValueCoding object, String key)
        {
            Object newFocus = DocumentController.getSharedDocumentController().getFocusControl();
            if (lastFocus != newFocus)
            {
                stopListening();
                if (newFocus != null)
                {
                    listenerMethods = getListenerMethods(newFocus);
                    if (listenerMethods != null)
                    {
                        try
                        {
                            listenerMethods.adder.invoke(actionName, availabilityListener);
                        } catch (InvocationTargetException ex) {
                            ex.printStackTrace();
                        } catch (IllegalAccessException ex) {
                            ex.printStackTrace();
                        }
                        listening = true;
                    }
                }
                lastFocus = newFocus;
                updateAvailable();
            }
        }

        /**
         * If we've added any listeners, remove them.
         */
        public void stopListening()
        {
            if (listening)
            {
                try
                {
                    listenerMethods.remover.invoke(actionName, availabilityListener);
                } catch (InvocationTargetException ex){
                    ex.printStackTrace();
                } catch (IllegalAccessException ex){
                    ex.printStackTrace();
                }
                listening = false;
            }
        }

        /**
         * Check to see if the item is now available
         */
        private void updateAvailable()
        {
            boolean available = (lastFocus != null && !actionDisabled(lastFocus, actionName) && (findActionMethod(lastFocus, actionName) != null));
            menuItem.setEnabled(available);
        }
    }

}
